fix(images): keep edits URL on retry for openai/azure - #2131
Conversation
OpenAI and Azure gpt-image-* edits return a multipart FormData body, and chat.ts swaps the upstream URL from /images/generations to /images/edits to match. That swap only ran on the initial path; applyResolvedProviderContext overwrote url from resolveProviderContext (which only knows the base /images/generations endpoint) without re-running the swap, so any retry fallback (alternate key, HTTP error retry, etc.) sent the multipart body to /images/generations and OpenAI rejected it with "Unsupported content type: 'multipart/form-data'". Mirror the swap inside applyResolvedProviderContext so retries hit the same endpoint as the initial attempt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WalkthroughThe PR extends URL rewriting for image generation endpoints to the retry/fallback path. It rewrites upstream URLs from Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/gateway/src/chat/chat.ts (1)
4046-4073: ⚡ Quick winConsolidate
/images/editsURL rewrite logic into one helperThis retry block now duplicates the same rewrite rules already present around Line 3913. That duplication caused this bug once, and keeping both copies in sync is brittle.
♻️ Suggested refactor
+function rewriteImageEditsUrl( + url: string | undefined, + isImageGeneration: boolean, + usedProvider: string, + requestBody: ProviderRequestBody | FormData, +): string | undefined { + if (!isImageGeneration || !url) { + return url; + } + if (usedProvider === "openai" && requestBody instanceof FormData) { + return url.replace("/v1/images/generations", "/v1/images/edits"); + } + if (usedProvider === "azure" && requestBody instanceof FormData) { + return url.replace("/images/generations", "/images/edits"); + } + if ( + usedProvider === "xai" && + !(requestBody instanceof FormData) && + ("image" in requestBody || "images" in requestBody) + ) { + return url.replace("/v1/images/generations", "/v1/images/edits"); + } + return url; +}Then replace both rewrite blocks with:
-// provider-specific rewrite block... +url = rewriteImageEditsUrl(url, isImageGeneration, usedProvider, requestBody);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/gateway/src/chat/chat.ts` around lines 4046 - 4073, Duplicate URL-rewrite logic for mapping generation->edits should be consolidated: add a helper function (e.g., rewriteImageEditsUrl(url: string | undefined, usedProvider: string, requestBody: any, isImageGeneration: boolean): string | undefined) that encapsulates the three branch rules (openai FormData replace "/v1/images/generations"->"/v1/images/edits", azure FormData replace "/images/generations"->"/images/edits", xai non-FormData with "image"/"images" keys replace "/v1/images/generations"->"/v1/images/edits") and returns the possibly-modified url; then replace both places in chat.ts where the same logic appears (the retry block around this diff and the earlier block around line ~3913) by calling rewriteImageEditsUrl(url, usedProvider, requestBody, isImageGeneration) so the behavior is centralized and future changes apply in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 4046-4073: Duplicate URL-rewrite logic for mapping
generation->edits should be consolidated: add a helper function (e.g.,
rewriteImageEditsUrl(url: string | undefined, usedProvider: string, requestBody:
any, isImageGeneration: boolean): string | undefined) that encapsulates the
three branch rules (openai FormData replace
"/v1/images/generations"->"/v1/images/edits", azure FormData replace
"/images/generations"->"/images/edits", xai non-FormData with "image"/"images"
keys replace "/v1/images/generations"->"/v1/images/edits") and returns the
possibly-modified url; then replace both places in chat.ts where the same logic
appears (the retry block around this diff and the earlier block around line
~3913) by calling rewriteImageEditsUrl(url, usedProvider, requestBody,
isImageGeneration) so the behavior is centralized and future changes apply in
one place.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 7a0ce855-c204-4ce4-b59f-9844aef0c946
📒 Files selected for processing (1)
apps/gateway/src/chat/chat.ts
Summary
OpenAI gpt-image-2 image-edit requests (FormData / multipart) were failing with:
…on retries (e.g. alternate-key fallback, post-error retry). Azure happened to mask the same bug because we tested it less frequently against this code path.
Root cause
For OpenAI/Azure gpt-image-* edits,
prepareRequestBodyreturns a multipartFormData, andchat.tsswaps the upstream URL from/images/generationsto/images/editsto match. That swap was only applied on the initial pass.applyResolvedProviderContextthen overwroteurlfromresolveProviderContext, which only knows the base/images/generationsendpoint, and never re-ran the swap. Any retry (same-provider alternate key or HTTP-error fallback) shipped the multipart body to/images/generations, and OpenAI 400'd.Fix
Mirror the same OpenAI / Azure / xAI URL-to-
/editsswap insideapplyResolvedProviderContext, so retries land on the same endpoint as the initial attempt.Test plan
/v1/images/edits/v1/images/generations/v1/images/edits🤖 Generated with Claude Code
Summary by CodeRabbit